﻿#region MIT
/*
Engine Of Evermore (http://code.google.com/p/engineofevermore/)
Copyright (c) 2008, 2009 Michael Woerister

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace EngineOfEvermore.Editor.Common.Command
{
    public class CommandStack : ICommandStack
    {
        #region class CommandEntry
        class CommandEntry
        {
            public ICommand Command;
            public int Version;
        }
        #endregion

        #region Variables
        bool _disposed = false;
        List<CommandEntry> _commandStack = new List<CommandEntry>();
        int _commandIndex = -1;
        int _maxVersion = -1;
        #endregion

        #region Finalizer
        ~CommandStack()
        {
            Dispose( false );
        }
        #endregion

        #region Events
        public event Action<CommandStack> CanUndoChanged;
        public event Action<CommandStack> CanRedoChanged;
        public event Action<CommandStack> VersionChanged;
        #endregion

        #region Properties
        /// <summary>
        /// Returns true if an undo is currently possible.
        /// </summary>
        public bool CanUndo
        {
            get
            {
                return _commandStack.Any() && _commandIndex >= 0;
            }
        }

        /// <summary>
        /// Returns true if a redo is currently possible.
        /// </summary>
        public bool CanRedo
        {
            get
            {
                bool isLastAction = ( _commandIndex == _commandStack.Count - 1 );
                return !isLastAction && _commandStack.Any();
            }
        }

        public int Version
        {
            get
            {
                if ( _commandIndex >= 0 && _commandIndex < _commandStack.Count )
                {
                    return _commandStack[_commandIndex].Version;
                }
                else
                {
                    return -1;
                }
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Performs an undo.
        /// </summary>
        public void Undo()
        {
            if ( !CanUndo )
            {
            	throw new System.InvalidOperationException("Undo is not possible");
            }

            Debug.Assert( _commandIndex >= 0 );
            Debug.Assert( _commandIndex < _commandStack.Count );

            bool oldCanUndoState = this.CanUndo;
            bool oldCanRedoState = this.CanRedo;
            int oldVersion = this.Version;

            // Take one command from the list
            ICommand command = _commandStack[_commandIndex--].Command;

            command.Undo();

            _raiseEventsIfNecessary( oldCanUndoState, oldCanRedoState, oldVersion );
        }

        /// <summary>
        /// Performs a redo.
        /// </summary>
        public void Redo()
        {
            if ( !CanRedo )
            {
            	throw new System.InvalidOperationException("Redo is not possible");
            }

            Debug.Assert( _commandIndex >= -1 );
            Debug.Assert( _commandIndex < _commandStack.Count - 1 );

            bool oldCanUndoState = this.CanUndo;
            bool oldCanRedoState = this.CanRedo;
            int oldVersion = this.Version;

            // Take next command in the list
            ICommand command = _commandStack[++_commandIndex].Command;

            command.Redo();

            _raiseEventsIfNecessary( oldCanUndoState, oldCanRedoState, oldVersion );
        }

        /// <summary>
        /// Pushes a command onto the stack.
        /// </summary>
        /// <remarks>
        /// The current command is the command that ICommand.Undo() would be called on if 
        /// CommandStack.Undo() would be called.
        /// If the current command is not the top-most command on the stack, then all commands 
        /// above it will be dropped from the stack. If any of the commands implements IDisposable
        /// then Dispose() will be called on them.
        /// </remarks>
        public void PushCommand( ICommand command )
        {
            Debug.Assert( command != null );

            bool oldCanUndoState = this.CanUndo;
            bool oldCanRedoState = this.CanRedo;
            int oldVersion = this.Version;

            // If the _commandIndex is not at the end of the
            // list remove anything after it
            _truncate();

            CommandEntry entry = new CommandEntry();
            entry.Command = command;
            entry.Version = ++_maxVersion;

            _commandStack.Add( entry );
            ++_commandIndex;

            _raiseEventsIfNecessary( oldCanUndoState, oldCanRedoState, oldVersion );
        }

        /// <summary>
        /// Drops all commands from the stack above the current command.
        /// </summary>
        /// <remarks>
        /// The current command is the command that ICommand.Undo() would be called on if 
        /// CommandStack.Undo() would be called.
        /// If the current command is not the top-most command on the stack, then all commands 
        /// above it will be dropped from the stack. If any of the commands implements IDisposable
        /// then Dispose() will be called on them.
        /// </remarks>
        public void Truncate()
        {
            bool oldCanUndoState = this.CanUndo;
            bool oldCanRedoState = this.CanRedo;
            int oldVersion = this.Version;

            _truncate();

            _raiseEventsIfNecessary( oldCanUndoState, oldCanRedoState, oldVersion );
        }

        void _raiseEventsIfNecessary( bool oldCanUndoState, bool oldCanRedoState, int oldVersion )
        {
            // Fire CanUndoChanged event if necessary
            if ( this.CanUndo != oldCanUndoState && this.CanUndoChanged != null )
            {
                this.CanUndoChanged( this );
            }

            // Fire CanRedoChanged event if necessary
            if ( this.CanRedo != oldCanRedoState && this.CanRedoChanged != null )
            {
                this.CanRedoChanged( this );
            }

            // Fire VersionChanged event 
            if ( this.Version != oldVersion && this.VersionChanged != null )
            {
                this.VersionChanged( this );
            }
        }

        // does the same as Truncate() but without raising events
        void _truncate()
        {
            if ( _commandIndex != _commandStack.Count - 1 )
            {
                for ( int i = _commandIndex + 1; i < _commandStack.Count; ++i )
                {
                    IDisposable disposable =
                        ( _commandStack[i].Command as IDisposable );

                    if ( disposable != null )
                    {
                        disposable.Dispose();
                    }
                }

                _commandStack.RemoveRange(
                    _commandIndex + 1,
                    _commandStack.Count - ( _commandIndex + 1 )
                    );
            }
        }
        #endregion

        #region IDisposable Members

        protected virtual void Dispose( bool disposing )
        {
            if ( !_disposed )
            {
                if ( disposing )
                {
                    foreach ( IDisposable disposable in
                        _commandStack.Select( x => x.Command ).OfType<IDisposable>() )
                    {
                        if ( disposable != null )
                        {
                            disposable.Dispose();
                        }
                    }

                    _commandStack.Clear();
                    _commandStack = null;
                }

                _disposed = true;
            }
        }

        public void Dispose()
        {
            Dispose( true );

            GC.SuppressFinalize( this );
        }

        #endregion
    }

    #if ENGINE_OF_EVERMORE_ENABLE_UNIT_TESTS
    namespace Test
    {
    	using NUnit.Framework;
		
		[TestFixture]
		public class TestCommandStack
		{
			[Test]
			public void TestUndo()
			{
				CommandStack teststack = new CommandStack();
				bool undoWasTriggered = false;
				bool versionWasTriggered = false;
				TestCommand testcmd1, testcmd2;
				testcmd1 = new TestCommand( "test1" );
				testcmd2 = new TestCommand( "test2" );
				
				teststack.CanUndoChanged += delegate( CommandStack obj ) { undoWasTriggered = true; };
				teststack.VersionChanged += delegate( CommandStack obj ) { versionWasTriggered = true; };
				
				teststack.PushCommand( testcmd1 );
				teststack.PushCommand( testcmd2 );
				
				teststack.Undo();
				teststack.Undo();
				
				Assert.That( teststack.CanUndo, Is.False );
				Assert.That( testcmd1.UndoCounter, Is.EqualTo( 1 ) );
				Assert.That( testcmd2.UndoCounter, Is.EqualTo( 1  ) );
				Assert.That( undoWasTriggered, Is.True );
				Assert.That( versionWasTriggered, Is.True );
			}
			
			[Test]
			[ExpectedException( "System.InvalidOperationException" )]
			public void TestUndoFail()
			{
				CommandStack teststack = new CommandStack();
				
				teststack.Undo();
			}
			
			[Test]
			public void TestRedo()
			{
				CommandStack teststack = new CommandStack();
				bool redoWasTriggered = false;
				bool versionWasTriggered = false;
				TestCommand testcmd1, testcmd2, testcmd3;
				testcmd1 = new TestCommand( "test1" );
				testcmd2 = new TestCommand( "test2" );
				testcmd3 = new TestCommand( "test3" );
				
				teststack.CanRedoChanged += delegate( CommandStack obj ) { redoWasTriggered = true; };
				teststack.VersionChanged += delegate( CommandStack obj ) { versionWasTriggered = true; };
				
				teststack.PushCommand( testcmd1 );
				teststack.PushCommand( testcmd2 );
				teststack.PushCommand( testcmd3 );

				teststack.Undo();
				teststack.Undo();
				teststack.Redo();			
				
				Assert.That( teststack.CanUndo, Is.True );
				Assert.That( teststack.CanRedo, Is.True );
				Assert.That( testcmd1.UndoCounter, Is.EqualTo( 0 ) );
				Assert.That( testcmd1.RedoCounter, Is.EqualTo( 0 ) );
				Assert.That( testcmd2.UndoCounter, Is.EqualTo( 1 ) );
				Assert.That( testcmd2.RedoCounter, Is.EqualTo( 1 ) );
				Assert.That( testcmd3.UndoCounter, Is.EqualTo( 1 ) );
				Assert.That( testcmd3.RedoCounter, Is.EqualTo( 0 ) );
				Assert.That( redoWasTriggered, Is.True );
				Assert.That( versionWasTriggered, Is.True );
			}
			
			[Test]
			[ExpectedException( "System.InvalidOperationException" )]
			public void TestRedoFail()
			{
				CommandStack teststack = new CommandStack();
				TestCommand testcmd1, testcmd2;
				testcmd1 = new TestCommand( "test1" );
				testcmd2 = new TestCommand( "test2" );
				
				teststack.PushCommand( testcmd1 );
				teststack.PushCommand( testcmd2 );

				teststack.Undo();
				teststack.Redo();
				teststack.Redo();
			}
			
			[Test]
			public void TestTruncateExplicit()
			{
				CommandStack teststack = new CommandStack();
				TestCommand testcmd1, testcmd2, testcmd3, testcmd4;
				testcmd1 = new TestCommand( "test1" );
				testcmd2 = new TestCommand( "test2" );
				testcmd3 = new TestCommand( "test3" );
				testcmd4 = new TestCommand( "test4" );
				
				teststack.PushCommand( testcmd1 );
				teststack.PushCommand( testcmd2 );
				teststack.PushCommand( testcmd3 );
				teststack.PushCommand( testcmd4 );
				
				teststack.Undo();
				teststack.Undo(); // only testcmd1 & testcmd2 are on the stack now
				
				teststack.Truncate(); // make redo of testcmd3 & testcmd4 impossible
				
				Assert.That( teststack.CanUndo, Is.True );
				Assert.That( teststack.CanRedo, Is.False );
				Assert.That( testcmd3.IsDisposed, Is.True );
				Assert.That( testcmd4.IsDisposed, Is.True );				
			}
			
			[Test]
			public void TestTruncateImplicit()
			{
				CommandStack teststack = new CommandStack();
				TestCommand testcmd1, testcmd2, testcmd3;
				testcmd1 = new TestCommand( "test1" );
				testcmd2 = new TestCommand( "test2" );
				testcmd3 = new TestCommand( "test3" );
				
				teststack.PushCommand( testcmd1 );
				teststack.PushCommand( testcmd2 );
				
				teststack.Undo();
				
				teststack.PushCommand( testcmd3 ); // only two objects on the stack
				
				teststack.Undo();
				teststack.Undo();
				
				Assert.That( teststack.CanUndo, Is.False );
				Assert.That( teststack.CanRedo, Is.True );
				Assert.That( testcmd1.UndoCounter, Is.EqualTo( 1 ) );
				Assert.That( testcmd1.RedoCounter, Is.EqualTo( 0 ) );
				Assert.That( testcmd2.UndoCounter, Is.EqualTo( 1 ) );
				Assert.That( testcmd2.RedoCounter, Is.EqualTo( 0 ) );
				Assert.That( testcmd3.UndoCounter, Is.EqualTo( 1 ) );
				Assert.That( testcmd3.RedoCounter, Is.EqualTo( 0 ) );
				Assert.That( testcmd2.IsDisposed, Is.True );
			}
			
			[Test]
			public void TestDispose()
			{
				CommandStack teststack = new CommandStack();
				TestCommand testcmd1, testcmd2;
				testcmd1 = new TestCommand( "test1" );
				testcmd2 = new TestCommand( "test2" );
				
				teststack.PushCommand( testcmd1 );
				teststack.PushCommand( testcmd2 );
				
				teststack.Dispose();
				
				Assert.That( testcmd1.IsDisposed, Is.True );
				Assert.That( testcmd2.IsDisposed, Is.True );
			}
		}
	
		class TestCommand : ICommand, IDisposable
		{
			public String Description{ get; private set; }
			public int UndoCounter { get; private set; }
			public int RedoCounter { get; private set; }
			public bool IsDisposed { get; private set; }
			
			public TestCommand( String description )
			{
				Description = description;
				UndoCounter = 0;
				RedoCounter = 0;
				IsDisposed = false;
			}
			
			public void Undo()
			{
				UndoCounter ++;
			}
			
			public void Redo()
			{
				RedoCounter++;
			}
			
			public void Dispose()
			{
				this.IsDisposed = true;
			}

		}
    }
    #endif
}
